/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.replica.replicaisland;

import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;

/**
 * A 2D rectangular mesh. Can be drawn textured or untextured. This version is
 * modified from the original Grid.java (found in the SpriteText package in the
 * APIDemos Android sample) to support hardware vertex buffers and to insert
 * edges between grid squares for tiling.
 */
class Grid {
	private static final int FLOAT_SIZE = 4;
	private static final int FIXED_SIZE = 4;
	private static final int CHAR_SIZE = 2;

	private FloatBuffer mFloatVertexBuffer;
	private FloatBuffer mFloatTexCoordBuffer;
	private IntBuffer mFixedVertexBuffer;
	private IntBuffer mFixedTexCoordBuffer;
	private CharBuffer mIndexBuffer;

	private Buffer mVertexBuffer;
	private Buffer mTexCoordBuffer;
	private int mCoordinateSize;
	private int mCoordinateType;

	private int mVertsAcross;
	private int mVertsDown;
	private int mIndexCount;
	private boolean mUseHardwareBuffers;
	private int mVertBufferIndex;
	private int mIndexBufferIndex;
	private int mTextureCoordBufferIndex;

	public Grid(int quadsAcross, int quadsDown, boolean useFixedPoint) {
		final int vertsAcross = quadsAcross * 2;
		final int vertsDown = quadsDown * 2;
		if (vertsAcross < 0 || vertsAcross >= 65536) {
			throw new IllegalArgumentException("quadsAcross");
		}
		if (vertsDown < 0 || vertsDown >= 65536) {
			throw new IllegalArgumentException("quadsDown");
		}
		if (vertsAcross * vertsDown >= 65536) {
			throw new IllegalArgumentException(
					"quadsAcross * quadsDown >= 32768");
		}

		mUseHardwareBuffers = false;

		mVertsAcross = vertsAcross;
		mVertsDown = vertsDown;
		int size = vertsAcross * vertsDown;

		if (useFixedPoint) {
			mFixedVertexBuffer = ByteBuffer
					.allocateDirect(FIXED_SIZE * size * 3)
					.order(ByteOrder.nativeOrder()).asIntBuffer();
			mFixedTexCoordBuffer = ByteBuffer
					.allocateDirect(FIXED_SIZE * size * 2)
					.order(ByteOrder.nativeOrder()).asIntBuffer();

			mVertexBuffer = mFixedVertexBuffer;
			mTexCoordBuffer = mFixedTexCoordBuffer;
			mCoordinateSize = FIXED_SIZE;
			mCoordinateType = GL10.GL_FIXED;

		} else {
			mFloatVertexBuffer = ByteBuffer
					.allocateDirect(FLOAT_SIZE * size * 3)
					.order(ByteOrder.nativeOrder()).asFloatBuffer();
			mFloatTexCoordBuffer = ByteBuffer
					.allocateDirect(FLOAT_SIZE * size * 2)
					.order(ByteOrder.nativeOrder()).asFloatBuffer();

			mVertexBuffer = mFloatVertexBuffer;
			mTexCoordBuffer = mFloatTexCoordBuffer;
			mCoordinateSize = FLOAT_SIZE;
			mCoordinateType = GL10.GL_FLOAT;
		}

		int quadCount = quadsAcross * quadsDown;
		int indexCount = quadCount * 6;
		mIndexCount = indexCount;
		mIndexBuffer = ByteBuffer.allocateDirect(CHAR_SIZE * indexCount)
				.order(ByteOrder.nativeOrder()).asCharBuffer();

		/*
		 * Initialize triangle list mesh.
		 * 
		 * [0]------[1] [2]------[3] ... | / | | / | | / | | / | | / | | / |
		 * [w]-----[w+1] [w+2]----[w+3]... | |
		 */

		{
			int i = 0;
			for (int y = 0; y < quadsDown; y++) {
				final int indexY = y * 2;
				for (int x = 0; x < quadsAcross; x++) {
					final int indexX = x * 2;
					char a = (char) (indexY * mVertsAcross + indexX);
					char b = (char) (indexY * mVertsAcross + indexX + 1);
					char c = (char) ((indexY + 1) * mVertsAcross + indexX);
					char d = (char) ((indexY + 1) * mVertsAcross + indexX + 1);

					mIndexBuffer.put(i++, a);
					mIndexBuffer.put(i++, b);
					mIndexBuffer.put(i++, c);

					mIndexBuffer.put(i++, b);
					mIndexBuffer.put(i++, c);
					mIndexBuffer.put(i++, d);
				}
			}
		}

		mVertBufferIndex = 0;
	}

	public void set(int quadX, int quadY, float[][] positions, float[][] uvs) {
		if (quadX < 0 || quadX * 2 >= mVertsAcross) {
			throw new IllegalArgumentException("quadX");
		}
		if (quadY < 0 || quadY * 2 >= mVertsDown) {
			throw new IllegalArgumentException("quadY");
		}
		if (positions.length < 4) {
			throw new IllegalArgumentException("positions");
		}
		if (uvs.length < 4) {
			throw new IllegalArgumentException("quadY");
		}

		int i = quadX * 2;
		int j = quadY * 2;

		setVertex(i, j, positions[0][0], positions[0][1], positions[0][2],
				uvs[0][0], uvs[0][1]);
		setVertex(i + 1, j, positions[1][0], positions[1][1], positions[1][2],
				uvs[1][0], uvs[1][1]);
		setVertex(i, j + 1, positions[2][0], positions[2][1], positions[2][2],
				uvs[2][0], uvs[2][1]);
		setVertex(i + 1, j + 1, positions[3][0], positions[3][1],
				positions[3][2], uvs[3][0], uvs[3][1]);
	}

	private void setVertex(int i, int j, float x, float y, float z, float u,
			float v) {
		if (i < 0 || i >= mVertsAcross) {
			throw new IllegalArgumentException("i");
		}
		if (j < 0 || j >= mVertsDown) {
			throw new IllegalArgumentException("j");
		}

		final int index = mVertsAcross * j + i;

		final int posIndex = index * 3;
		final int texIndex = index * 2;

		if (mCoordinateType == GL10.GL_FLOAT) {
			mFloatVertexBuffer.put(posIndex, x);
			mFloatVertexBuffer.put(posIndex + 1, y);
			mFloatVertexBuffer.put(posIndex + 2, z);

			mFloatTexCoordBuffer.put(texIndex, u);
			mFloatTexCoordBuffer.put(texIndex + 1, v);
		} else {
			mFixedVertexBuffer.put(posIndex, (int) (x * (1 << 16)));
			mFixedVertexBuffer.put(posIndex + 1, (int) (y * (1 << 16)));
			mFixedVertexBuffer.put(posIndex + 2, (int) (z * (1 << 16)));

			mFixedTexCoordBuffer.put(texIndex, (int) (u * (1 << 16)));
			mFixedTexCoordBuffer.put(texIndex + 1, (int) (v * (1 << 16)));
		}
	}

	public static void beginDrawing(GL10 gl, boolean useTexture) {
		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

		if (useTexture) {
			gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
			gl.glEnable(GL10.GL_TEXTURE_2D);
		} else {
			gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
			gl.glDisable(GL10.GL_TEXTURE_2D);
		}
	}

	public void beginDrawingStrips(GL10 gl, boolean useTexture) {
		beginDrawing(gl, useTexture);
		if (!mUseHardwareBuffers) {
			gl.glVertexPointer(3, mCoordinateType, 0, mVertexBuffer);

			if (useTexture) {
				gl.glTexCoordPointer(2, mCoordinateType, 0, mTexCoordBuffer);
			}

		} else {
			GL11 gl11 = (GL11) gl;
			// draw using hardware buffers
			gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mVertBufferIndex);
			gl11.glVertexPointer(3, mCoordinateType, 0, 0);

			gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mTextureCoordBufferIndex);
			gl11.glTexCoordPointer(2, mCoordinateType, 0, 0);

			gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, mIndexBufferIndex);
		}
	}

	// Assumes beginDrawingStrips() has been called before this.
	public void drawStrip(GL10 gl, boolean useTexture, int startIndex,
			int indexCount) {
		int count = indexCount;
		if (startIndex + indexCount >= mIndexCount) {
			count = mIndexCount - startIndex;
		}
		if (!mUseHardwareBuffers) {
			gl.glDrawElements(GL10.GL_TRIANGLES, count, GL10.GL_UNSIGNED_SHORT,
					mIndexBuffer.position(startIndex));
		} else {
			GL11 gl11 = (GL11) gl;
			gl11.glDrawElements(GL11.GL_TRIANGLES, count,
					GL11.GL_UNSIGNED_SHORT, startIndex * CHAR_SIZE);

		}
	}

	public void draw(GL10 gl, boolean useTexture) {
		if (!mUseHardwareBuffers) {
			gl.glVertexPointer(3, mCoordinateType, 0, mVertexBuffer);

			if (useTexture) {
				gl.glTexCoordPointer(2, mCoordinateType, 0, mTexCoordBuffer);
			}

			gl.glDrawElements(GL10.GL_TRIANGLES, mIndexCount,
					GL10.GL_UNSIGNED_SHORT, mIndexBuffer);
		} else {
			GL11 gl11 = (GL11) gl;
			// draw using hardware buffers
			gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mVertBufferIndex);
			gl11.glVertexPointer(3, mCoordinateType, 0, 0);

			gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mTextureCoordBufferIndex);
			gl11.glTexCoordPointer(2, mCoordinateType, 0, 0);

			gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, mIndexBufferIndex);
			gl11.glDrawElements(GL11.GL_TRIANGLES, mIndexCount,
					GL11.GL_UNSIGNED_SHORT, 0);

			gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0);
			gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0);

		}
	}

	public static void endDrawing(GL10 gl) {
		gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
	}

	public boolean usingHardwareBuffers() {
		return mUseHardwareBuffers;
	}

	/**
	 * When the OpenGL ES device is lost, GL handles become invalidated. In that
	 * case, we just want to "forget" the old handles (without explicitly
	 * deleting them) and make new ones.
	 */
	public void invalidateHardwareBuffers() {
		mVertBufferIndex = 0;
		mIndexBufferIndex = 0;
		mTextureCoordBufferIndex = 0;
		mUseHardwareBuffers = false;
	}

	/**
	 * Deletes the hardware buffers allocated by this object (if any).
	 */
	public void releaseHardwareBuffers(GL10 gl) {
		if (mUseHardwareBuffers) {
			if (gl instanceof GL11) {
				GL11 gl11 = (GL11) gl;
				int[] buffer = new int[1];
				buffer[0] = mVertBufferIndex;
				gl11.glDeleteBuffers(1, buffer, 0);

				buffer[0] = mTextureCoordBufferIndex;
				gl11.glDeleteBuffers(1, buffer, 0);

				buffer[0] = mIndexBufferIndex;
				gl11.glDeleteBuffers(1, buffer, 0);
			}

			invalidateHardwareBuffers();
		}
	}

	/**
	 * Allocates hardware buffers on the graphics card and fills them with data
	 * if a buffer has not already been previously allocated. Note that this
	 * function uses the GL_OES_vertex_buffer_object extension, which is not
	 * guaranteed to be supported on every device.
	 * 
	 * @param gl
	 *            A pointer to the OpenGL ES context.
	 */
	public void generateHardwareBuffers(GL10 gl) {
		if (!mUseHardwareBuffers) {
			DebugLog.i("Grid", "Using Hardware Buffers");
			if (gl instanceof GL11) {
				GL11 gl11 = (GL11) gl;
				int[] buffer = new int[1];

				// Allocate and fill the vertex buffer.
				gl11.glGenBuffers(1, buffer, 0);
				mVertBufferIndex = buffer[0];
				gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mVertBufferIndex);
				final int vertexSize = mVertexBuffer.capacity()
						* mCoordinateSize;
				gl11.glBufferData(GL11.GL_ARRAY_BUFFER, vertexSize,
						mVertexBuffer, GL11.GL_STATIC_DRAW);

				// Allocate and fill the texture coordinate buffer.
				gl11.glGenBuffers(1, buffer, 0);
				mTextureCoordBufferIndex = buffer[0];
				gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER,
						mTextureCoordBufferIndex);
				final int texCoordSize = mTexCoordBuffer.capacity()
						* mCoordinateSize;
				gl11.glBufferData(GL11.GL_ARRAY_BUFFER, texCoordSize,
						mTexCoordBuffer, GL11.GL_STATIC_DRAW);

				// Unbind the array buffer.
				gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0);

				// Allocate and fill the index buffer.
				gl11.glGenBuffers(1, buffer, 0);
				mIndexBufferIndex = buffer[0];
				gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER,
						mIndexBufferIndex);
				// A char is 2 bytes.
				final int indexSize = mIndexBuffer.capacity() * 2;
				gl11.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER, indexSize,
						mIndexBuffer, GL11.GL_STATIC_DRAW);

				// Unbind the element array buffer.
				gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0);

				mUseHardwareBuffers = true;

				assert mVertBufferIndex != 0;
				assert mTextureCoordBufferIndex != 0;
				assert mIndexBufferIndex != 0;
				assert gl11.glGetError() == 0;

			}
		}
	}

}
